| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- 'use client';
- // D2 종목 글쓰기 폼 — 제목/본문 + 선택적 예측(방향·기간·목표가).
- // POST /api/boards/stock/posts (프록시 경유, JWT). 성공 시 라우터 새로고침으로 목록 갱신.
- import '@/app/styles/community.scss';
- import { useState } from 'react';
- import { useRouter } from 'next/navigation';
- import { fetchApi } from '@/lib/utils/client';
- import {
- CreateStockPostRequest,
- CreateStockPostResponse,
- PredictionDirection,
- PredictionHorizon,
- PredictionDirectionValue,
- PredictionHorizonValue
- } from '@/types/community';
- interface Props {
- stockCode: string;
- stockName?: string|null;
- onDone?: () => void;
- }
- export default function StockWriteForm({ stockCode, stockName, onDone }: Props)
- {
- const router = useRouter();
- const [subject, setSubject] = useState('');
- const [content, setContent] = useState('');
- const [withPrediction, setWithPrediction] = useState(false);
- const [direction, setDirection] = useState<PredictionDirectionValue>(PredictionDirection.Up);
- const [horizon, setHorizon] = useState<PredictionHorizonValue>(PredictionHorizon.Short);
- const [targetPrice, setTargetPrice] = useState('');
- const [submitting, setSubmitting] = useState(false);
- const [error, setError] = useState<string|null>(null);
- async function handleSubmit(e: React.FormEvent) {
- e.preventDefault();
- setError(null);
- if (!subject.trim()) {
- setError('제목을 입력해주세요.');
- return;
- }
- const body: CreateStockPostRequest = {
- stockCode,
- subject: subject.trim(),
- content
- };
- if (withPrediction) {
- const parsedTarget = targetPrice.trim() ? Number(targetPrice.replace(/,/g, '')) : null;
- if (parsedTarget !== null && (Number.isNaN(parsedTarget) || parsedTarget <= 0)) {
- setError('목표가는 0보다 큰 숫자여야 합니다.');
- return;
- }
- body.prediction = {
- direction,
- horizonDays: horizon,
- targetPrice: parsedTarget
- };
- }
- setSubmitting(true);
- try {
- const res = await fetchApi<CreateStockPostResponse>('/api/boards/stock/posts', {
- method: 'POST',
- body,
- silent: true
- });
- if (!res.success) {
- setError(res.message || (res.errors && res.errors[0]?.description) || '글 등록에 실패했습니다.');
- setSubmitting(false);
- return;
- }
- setSubject('');
- setContent('');
- setWithPrediction(false);
- setTargetPrice('');
- onDone?.();
- router.refresh();
- } catch (err) {
- setError(err instanceof Error ? err.message : '글 등록에 실패했습니다.');
- } finally {
- setSubmitting(false);
- }
- }
- return (
- <form className='stock-write' onSubmit={handleSubmit}>
- <div className='stock-write__title'>{stockName ? `${stockName}(${stockCode})` : stockCode} 토론 글쓰기</div>
- <div className='stock-write__field'>
- <label htmlFor='stock-write-subject'>제목</label>
- <input
- id='stock-write-subject'
- type='text'
- value={subject}
- onChange={e => setSubject(e.target.value)}
- maxLength={200}
- placeholder='제목을 입력하세요'
- />
- </div>
- <div className='stock-write__field'>
- <label htmlFor='stock-write-content'>내용</label>
- <textarea
- id='stock-write-content'
- value={content}
- onChange={e => setContent(e.target.value)}
- placeholder='내용을 입력하세요. 다른 종목은 $005930 형식으로 태그할 수 있습니다.'
- ></textarea>
- </div>
- <div className='stock-write__prediction'>
- <div className='stock-write__prediction-head'>
- <label>
- <input
- type='checkbox'
- checked={withPrediction}
- onChange={e => setWithPrediction(e.target.checked)}
- />
- {' '}예측 등록 (방향·기간·목표가) — 채점되어 적중률에 반영됩니다
- </label>
- </div>
- {withPrediction && (
- <div className='stock-write__prediction-grid'>
- <div className='stock-write__field'>
- <label htmlFor='stock-write-direction'>방향</label>
- <select
- id='stock-write-direction'
- value={direction}
- onChange={e => setDirection(Number(e.target.value) as PredictionDirectionValue)}
- >
- <option value={PredictionDirection.Up}>▲ 상승</option>
- <option value={PredictionDirection.Down}>▼ 하락</option>
- </select>
- </div>
- <div className='stock-write__field'>
- <label htmlFor='stock-write-horizon'>기간</label>
- <select
- id='stock-write-horizon'
- value={horizon}
- onChange={e => setHorizon(Number(e.target.value) as PredictionHorizonValue)}
- >
- <option value={PredictionHorizon.Short}>5영업일</option>
- <option value={PredictionHorizon.Long}>20영업일</option>
- </select>
- </div>
- <div className='stock-write__field'>
- <label htmlFor='stock-write-target'>목표가 (선택)</label>
- <input
- id='stock-write-target'
- type='number'
- min='0'
- value={targetPrice}
- onChange={e => setTargetPrice(e.target.value)}
- placeholder='예: 75000'
- />
- </div>
- </div>
- )}
- <p className='stock-write__hint'>
- 예측 기준가는 작성 시점 최신 종가(T+1)로 스냅샷되며, 삭제해도 채점 기록은 유지됩니다.
- </p>
- </div>
- {error && <p className='stock-write__error' role='alert'>{error}</p>}
- <div className='stock-write__actions'>
- <button type='button' className='stock-write__btn' onClick={() => onDone?.()} disabled={submitting}>
- 취소
- </button>
- <button type='submit' className='stock-write__btn stock-write__btn--primary' disabled={submitting}>
- {submitting ? '등록 중...' : '등록'}
- </button>
- </div>
- </form>
- );
- }
|